{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "304af635",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "304af635",
        "outputId": "7b76d9b2-e4d5-49f8-b77c-aa3215c55c7b"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\u001b[?25l   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/2.5 MB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K   \u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[91m╸\u001b[0m \u001b[32m2.5/2.5 MB\u001b[0m \u001b[31m137.0 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m2.5/2.5 MB\u001b[0m \u001b[31m68.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
            "\u001b[?25h\u001b[?25l   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/45.2 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m45.2/45.2 kB\u001b[0m \u001b[31m3.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
            "\u001b[?25h\u001b[?25l   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/50.9 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m50.9/50.9 kB\u001b[0m \u001b[31m3.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
            "\u001b[?25h"
          ]
        }
      ],
      "source": [
        "!pip install -qU \\\n",
        "  langchain==0.3.25 \\\n",
        "  langchain-community==0.3.25 \\\n",
        "  langchain-openai==0.3.22 \\\n",
        "  numexpr==2.11.0"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7a4ba72d",
      "metadata": {
        "id": "7a4ba72d"
      },
      "source": [
        "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pinecone-io/examples/blob/master/learn/generation/langchain/handbook/02-langchain-chains.ipynb) [![Open nbviewer](https://raw.githubusercontent.com/pinecone-io/examples/master/assets/nbviewer-shield.svg)](https://nbviewer.org/github/pinecone-io/examples/blob/master/learn/generation/langchain/handbook/02-langchain-chains.ipynb)\n",
        "\n",
        "#### [LangChain Handbook](https://github.com/pinecone-io/examples/tree/master/learn/generation/langchain/handbook)\n",
        "\n",
        "# Getting Started with Chains\n",
        "\n",
        "Chains are the core of LangChain. They are simply a chain of components, executed in a particular order.\n",
        "\n",
        "The simplest of these chains is the `LLMChain`. It works by taking a user's input, passing in to the first element in the chain — a `PromptTemplate` — to format the input into a particular prompt. The formatted prompt is then passed to the next (and final) element in the chain — a LLM.\n",
        "\n",
        "Nowadays, chains are mostly built using **L**ang**C**hain **E**xpression **L**anguage (LCEL) rather that using objects like the `LLMChain`. So, when we refer to the `LLMChain` we are references this _past_ object but nowadays this would be implemented via LCEL syntax - which we'll explore soon.\n",
        "\n",
        "Let's start by importing all the libraries that we'll be using in this example."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "wPdWz1IdxyBR",
      "metadata": {
        "id": "wPdWz1IdxyBR"
      },
      "source": [
        "To run this notebook, we will need to use an OpenAI LLM. Here we will setup the LLM we will use throughout the notebook, just input your [OpenAI API key](https://platform.openai.com/api-keys) below."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "v86cmyppxdfc",
      "metadata": {
        "id": "v86cmyppxdfc"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "from getpass import getpass\n",
        "\n",
        "os.environ[\"OPENAI_API_KEY\"] = os.getenv(\"OPENAI_API_KEY\") \\\n",
        "    or getpass(\"Enter your OpenAI API key: \")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "baaa74b8",
      "metadata": {
        "id": "baaa74b8"
      },
      "outputs": [],
      "source": [
        "from langchain_openai import ChatOpenAI\n",
        "\n",
        "# initialize the models\n",
        "llm = ChatOpenAI(\n",
        "    model_name=\"gpt-4.1-mini\",\n",
        "    temperature=0.7\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6e1f31b4",
      "metadata": {
        "id": "6e1f31b4"
      },
      "source": [
        "## Chains and LCEL\n",
        "\n",
        "Chains in LangChain are now built using the LangChain Expression Language (LCEL), which takes a declarative approach to combining components. Instead of using predefined chain classes, LCEL lets you compose chains using the `|` operator and other composition primitives.\n",
        "\n",
        "### Types of Chain Composition\n",
        "\n",
        "1. **Sequential Chains** (`|` operator)\n",
        "   - Chain components one after another\n",
        "   - Example: `prompt | llm | output_parser`\n",
        "\n",
        "2. **Parallel Chains** (`RunnableParallel`)\n",
        "   - Run multiple operations concurrently\n",
        "   - Example: Running multiple prompts or retrievers in parallel\n",
        "\n",
        "3. **Complex Workflows**\n",
        "   - For more complex scenarios involving branching, cycles, or multiple agents\n",
        "   - Recommended to use LangGraph instead of LCEL directly\n",
        "\n",
        "Let's start with a simple example: creating a sequential math chain that can handle calculations..."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "b4161561",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "b4161561",
        "outputId": "304e1186-6ffc-4c29-c24d-6473b8ab9ebe"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The result is: 2.4116004626599237\n"
          ]
        }
      ],
      "source": [
        "import numexpr\n",
        "from langchain_core.prompts import ChatPromptTemplate\n",
        "from langchain_core.output_parsers import StrOutputParser\n",
        "from langchain_core.runnables import RunnableLambda\n",
        "\n",
        "# Create a function to handle calculations\n",
        "def calculate(expression: str) -> str:\n",
        "    \"\"\"Calculate using numexpr, with support for basic math operations.\"\"\"\n",
        "    try:\n",
        "        result = float(numexpr.evaluate(expression))\n",
        "        return f\"The result is: {result}\"\n",
        "    except Exception as e:\n",
        "        return f\"Error in calculation: {str(e)}\"\n",
        "\n",
        "# Create the prompt\n",
        "prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a helpful math assistant. When given a math problem, respond ONLY with the mathematical expression that would solve it. For example, if asked 'What is 2 raised to the 3rd power?', respond only with '2**3'.\"),\n",
        "    (\"user\", \"{question}\")\n",
        "])\n",
        "\n",
        "# Wrap our calculation function with RunnableLambda for explicit LCEL pattern\n",
        "calculate_runnable = RunnableLambda(calculate)\n",
        "\n",
        "# Create the chain using LCEL with explicit RunnableLambda\n",
        "math_chain = (\n",
        "    prompt\n",
        "    | ChatOpenAI(temperature=0)\n",
        "    | StrOutputParser()  # Convert to string\n",
        "    | calculate_runnable  # Our calculation function wrapped in RunnableLambda\n",
        ")\n",
        "\n",
        "# Use the chain with our example\n",
        "response = math_chain.invoke({\n",
        "    \"question\": \"What is 13 raised to the .3432 power?\"\n",
        "})\n",
        "print(response)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "86fecbca",
      "metadata": {
        "id": "86fecbca"
      },
      "source": [
        "Let's see what is going on here. The chain processes our input through several sequential steps:\n",
        "\n",
        "1. The prompt template formats our question\n",
        "2. The LLM converts it to a mathematical expression\n",
        "3. The StrOutputParser ensures we get a clean string\n",
        "4. Finally, our calculate function computes the result\n",
        "\n",
        "But how did the LLM know to return just the mathematical expression? 🤔\n",
        "\n",
        "**Enter prompts**\n",
        "\n",
        "The question we send isn't the only input the LLM receives 😉. Look at our prompt template:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "42224b54",
      "metadata": {
        "id": "42224b54"
      },
      "outputs": [],
      "source": [
        "prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a helpful math assistant. When given a math problem, respond ONLY with the mathematical expression that would solve it. For example, if asked 'What is 2 raised to the 3rd power?', respond only with '2**3'.\"),\n",
        "    (\"user\", \"{question}\")\n",
        "])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "abdb8561",
      "metadata": {
        "id": "abdb8561"
      },
      "source": [
        "The system message explicitly instructs the LLM to return only the mathematical expression. Without this context, the LLM would try to calculate the result itself. Let's test this by trying without the system message:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "8e477bc6",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "8e477bc6",
        "outputId": "da36b1b2-405c-4f08-a154-73d1c4f05e64"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "13^0.3432 is approximately equal to 2.732.\n"
          ]
        }
      ],
      "source": [
        "# Simple prompt without guidance\n",
        "prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"user\", \"{question}\")\n",
        "])\n",
        "\n",
        "basic_chain = (\n",
        "    prompt\n",
        "    | ChatOpenAI(temperature=0)\n",
        "    | StrOutputParser()\n",
        ")\n",
        "\n",
        "response = basic_chain.invoke({\n",
        "    \"question\": \"What is 13 raised to the .3432 power?\"\n",
        "})\n",
        "print(response)  # The LLM tries to calculate it directly and gets it wrong!\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "56da4f48",
      "metadata": {
        "id": "56da4f48"
      },
      "source": [
        "This demonstrates the power of prompting in LCEL: by carefully designing our prompts, we can guide the LLM's behavior precisely.\n",
        "\n",
        "The beauty of LCEL's sequential composition is how clearly we can see each step in the chain:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "0fb5f537",
      "metadata": {
        "id": "0fb5f537"
      },
      "outputs": [],
      "source": [
        "math_chain = (\n",
        "    prompt                         # Step 1: Format the input with our system message\n",
        "    | ChatOpenAI(temperature=0)    # Step 2: Get mathematical expression from LLM\n",
        "    | StrOutputParser()            # Step 3: Convert to clean string\n",
        "    | calculate                    # Step 4: Evaluate the expression\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9b4ea2a2",
      "metadata": {
        "id": "9b4ea2a2"
      },
      "source": [
        "Each step flows naturally into the next using the `|` operator, making it easy to understand and modify the chain's behavior. This is much more flexible than the old approach of using predefined chain classes - we can easily add, remove, or modify steps as needed!\n",
        "\n",
        "*_Note: The `calculate` function uses `numexpr` to safely evaluate mathematical expressions without needing a full Python REPL (Read-Eval-Print Loop)._"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f66a25a2",
      "metadata": {
        "id": "f66a25a2"
      },
      "source": [
        "### Building Complex Chains with LCEL\n",
        "\n",
        "Let's build a more complex example that shows how to combine different components using LCEL. We'll create a chain that cleans up messy text and then paraphrases it in a specific style.\n",
        "\n",
        "First, let's create a function to clean up text by removing extra spaces and newlines. In LCEL, we can use regular functions directly in our chain:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "41d95063",
      "metadata": {
        "id": "41d95063"
      },
      "outputs": [],
      "source": [
        "import re\n",
        "\n",
        "def clean_text(text: str) -> str:\n",
        "    # replace multiple new lines and multiple spaces with a single one\n",
        "    text = re.sub(r'(\\r\\n|\\r|\\n){2,}', r'\\n', text)\n",
        "    text = re.sub(r'[ \\t]+', ' ', text)\n",
        "    return text"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "98ca3a11",
      "metadata": {
        "id": "98ca3a11"
      },
      "source": [
        "Now, let's create our prompt template for the paraphrasing:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "71a5596e",
      "metadata": {
        "id": "71a5596e"
      },
      "outputs": [],
      "source": [
        "prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a creative writing assistant.\"),\n",
        "    (\"user\", \"\"\"Please paraphrase this text in the style of {style}:\n",
        "\n",
        "{text}\"\"\")\n",
        "])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9f5ff6c9",
      "metadata": {
        "id": "9f5ff6c9"
      },
      "source": [
        "Now we can combine everything into a sequential chain using LCEL's `|` operator. The beauty of LCEL is how naturally we can compose these components:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "268da3a9",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "268da3a9",
        "outputId": "7a5f807b-aac2-4571-d7b7-2b320d846750"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Yo, check it - chains in the game,\n",
            "Bringin' all the components together, ain't it insane?\n",
            "Craftin' a slick app that's all in sync,\n",
            "User input to PromptTemplate, then pass it to LLM, that's how we link.\n",
            "Mix and match, buildin' up them chains,\n",
            "Complexity on the rise, ain't no reins.\n",
            "Merge 'em all, blend with other parts,\n",
            "In the world of creation, we're makin' our marks.\n"
          ]
        }
      ],
      "source": [
        "# Create the chain using LCEL\n",
        "style_chain = (\n",
        "    {\n",
        "        \"text\": lambda x: clean_text(x[\"text\"]),  # Extract and clean the text from input dict\n",
        "        \"style\": lambda x: x[\"style\"]  # Extract style from input dict\n",
        "    }\n",
        "    | prompt  # Format with our template\n",
        "    | ChatOpenAI(temperature=0.7)  # Generate creative paraphrase\n",
        "    | StrOutputParser()  # Convert to string\n",
        ")\n",
        "\n",
        "# Our input text with messy spacing\n",
        "input_text = \"\"\"\n",
        "Chains allow us to combine multiple\n",
        "\n",
        "\n",
        "components together to create a single, coherent application.\n",
        "\n",
        "For example, we can create a chain that takes user input,       format it with a PromptTemplate,\n",
        "\n",
        "and then passes the formatted response to an LLM. We can build more complex chains by combining     multiple chains together, or by\n",
        "\n",
        "\n",
        "combining chains with other components.\n",
        "\"\"\"\n",
        "\n",
        "# Run the chain\n",
        "response = style_chain.invoke({\n",
        "    \"text\": input_text,\n",
        "    \"style\": \"a 90s rapper\"\n",
        "})\n",
        "print(response)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0b1f98f8",
      "metadata": {
        "id": "0b1f98f8"
      },
      "source": [
        "Let's look at how this chain works:\n",
        "\n",
        "1. The dictionary `{\"text\": clean_text, \"style\": lambda x: x}` processes our inputs in parallel using `RunnableParallel`\n",
        "2. The `|` operator connects each component, showing the clear flow of data\n",
        "3. Each step in the chain serves a specific purpose and is easily modifiable\n",
        "4. The components work together seamlessly to process and transform the text\n",
        "\n",
        "This demonstrates how LCEL lets us compose simple components into powerful chains while keeping the code readable and maintainable. Whether you're processing text, generating content, or building complex workflows, LCEL's composition primitives make it easy to build exactly what you need! 🔥"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bd8fb6aa",
      "metadata": {
        "id": "bd8fb6aa"
      },
      "source": [
        "### Using RunnableParallel and RunnablePassthrough\n",
        "\n",
        "Let's explore how to use `RunnableParallel` for running multiple operations concurrently and `RunnablePassthrough` for passing data through unchanged:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "6983ae77",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "6983ae77",
        "outputId": "75320ad3-8c53-433e-c9a3-aee689fc53b3"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Sentiment: The sentiment of the statement is positive. The use of words like \"exceeded my expectations\" and \"great quality\" indicates a high level of satisfaction and positivity towards the product.\n",
            "Summary: The product surpassed expectations with its excellent quality.\n",
            "Original: The product exceeded my expectations. Great quality!\n"
          ]
        }
      ],
      "source": [
        "from langchain_core.runnables import RunnableParallel, RunnablePassthrough\n",
        "\n",
        "# Create two different analysis prompts\n",
        "sentiment_prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a sentiment analysis expert. Analyze the emotional tone.\"),\n",
        "    (\"user\", \"What's the sentiment of: {text}\")\n",
        "])\n",
        "\n",
        "summary_prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a summarization expert.\"),\n",
        "    (\"user\", \"Summarize in one sentence: {text}\")\n",
        "])\n",
        "\n",
        "# Use RunnableParallel to run both analyses simultaneously\n",
        "analysis_chain = RunnableParallel(\n",
        "    {\n",
        "        \"sentiment\": sentiment_prompt | ChatOpenAI(temperature=0) | StrOutputParser(),\n",
        "        \"summary\": summary_prompt | ChatOpenAI(temperature=0) | StrOutputParser(),\n",
        "        \"original\": RunnablePassthrough()  # Pass through the original input\n",
        "    }\n",
        ")\n",
        "\n",
        "# Test it\n",
        "sample_text = {\"text\": \"The product exceeded my expectations. Great quality!\"}\n",
        "results = analysis_chain.invoke(sample_text)\n",
        "\n",
        "print(\"Sentiment:\", results[\"sentiment\"])\n",
        "print(\"Summary:\", results[\"summary\"])\n",
        "print(\"Original:\", results[\"original\"][\"text\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "290aa2aa",
      "metadata": {
        "id": "290aa2aa"
      },
      "source": [
        "### Batch Processing with LCEL\n",
        "\n",
        "LCEL chains support efficient batch processing using the `.batch()` method:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "2b626aba",
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "2b626aba",
        "outputId": "03272258-b5b7-4a97-c56d-d51cb15d288f"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Q: What is the capital of France?\n",
            "A: The capital of France is Paris.\n",
            "\n",
            "Q: Who wrote Romeo and Juliet?\n",
            "A: William Shakespeare wrote Romeo and Juliet.\n",
            "\n",
            "Q: What is the speed of light?\n",
            "A: The speed of light in a vacuum is approximately 299,792 kilometers per second (or about 186,282 miles per second).\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Create a simple question-answering chain\n",
        "qa_prompt = ChatPromptTemplate.from_messages([\n",
        "    (\"system\", \"You are a helpful assistant. Answer concisely.\"),\n",
        "    (\"user\", \"{question}\")\n",
        "])\n",
        "\n",
        "qa_chain = qa_prompt | ChatOpenAI(temperature=0) | StrOutputParser()\n",
        "\n",
        "# Batch of questions\n",
        "questions = [\n",
        "    {\"question\": \"What is the capital of France?\"},\n",
        "    {\"question\": \"Who wrote Romeo and Juliet?\"},\n",
        "    {\"question\": \"What is the speed of light?\"}\n",
        "]\n",
        "\n",
        "# Process all questions in batch\n",
        "answers = qa_chain.batch(questions)\n",
        "\n",
        "# Display results\n",
        "for q, a in zip(questions, answers):\n",
        "    print(f\"Q: {q['question']}\")\n",
        "    print(f\"A: {a}\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2cc688ca",
      "metadata": {
        "id": "2cc688ca"
      },
      "source": [
        "That's it for this example on chains with LCEL.\n",
        "\n",
        "---"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "pinecone1",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.11.4"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
